package com.anasoft.os.s4j;

import java.net.BindException;
import java.util.HashSet;
import java.util.Set;

import org.openqa.selenium.server.RemoteControlConfiguration;
import org.openqa.selenium.server.SeleniumServer;

import com.anasoft.os.s4j.annotation.EmbeddedSeleniumServer;
import com.anasoft.os.s4j.annotation.SeleniumSession;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.Selenium;

/**
 * Manages {@link Selenium} sessions and embedded Selenium servers used by
 * Selenium tests.
 * 
 * @see SeleniumSession
 * @see EmbeddedSeleniumServer
 * 
 * @author vojtech.szocs
 */
public class SeleniumSessionManager {

    private static final SeleniumSessionListener NOOP_SESSION_LISTENER = new SeleniumSessionListener() {
        public void beforeSessionStopped(Selenium selenium) {
            // no-op
        }

        public void beforeSessionStarted(Selenium selenium) {
            // no-op
        }

        public void afterSessionStopped(Selenium selenium) {
            // no-op
        }

        public void afterSessionStarted(Selenium selenium) {
            // no-op
        }
    };

    private static final EmbeddedSeleniumServerListener NOOP_SERVER_LISTENER = new EmbeddedSeleniumServerListener() {
        public void beforeServerStarted(RemoteControlConfiguration rcConfig) {
            // no-op
        }
    };

    private static final SeleniumInstanceFactory DEFAULT_SELENIUM_FACTORY = new SeleniumInstanceFactory() {
        public Selenium createSelenium(SeleniumSession sessionConfig) {
            return new DefaultSelenium(sessionConfig.serverHost(), sessionConfig.serverPort(), sessionConfig.browserString(), sessionConfig.browserUrl());
        }
    };

    private final SeleniumSession sessionConfig;

    private final SeleniumSessionListener sessionListener;
    private final EmbeddedSeleniumServerListener serverListener;
    private final SeleniumInstanceFactory seleniumFactory;

    // synchronization is done manually
    private static final Set<Integer> activeServerPorts = new HashSet<Integer>();

    /**
     * Creates new session manager.
     * 
     * @param sessionConfig {@link Selenium} session configuration.
     * @param runnerLogic Selenium test runner interface.
     */
    public SeleniumSessionManager(SeleniumSession sessionConfig, SeleniumRunnerLogic runnerLogic) {
        this.sessionConfig = sessionConfig;
        this.sessionListener = runnerLogic.getSessionListener() != null ? runnerLogic.getSessionListener() : NOOP_SESSION_LISTENER;
        this.serverListener = runnerLogic.getEmbeddedServerListener() != null ? runnerLogic.getEmbeddedServerListener() : NOOP_SERVER_LISTENER;
        this.seleniumFactory = runnerLogic.getSeleniumInstanceFactory() != null ? runnerLogic.getSeleniumInstanceFactory() : DEFAULT_SELENIUM_FACTORY;
    }

    /**
     * Executes the <tt>callback</tt> within a new {@link Selenium} session.
     * <p>
     * The session is active during <tt>callback</tt> execution. Code inside the
     * <tt>callback</tt> can access the session via provided
     * <tt>sessionHolder</tt>.
     * 
     * @param sessionHolder Thread-local variable in which the session should be
     *            stored during <tt>callback</tt> execution.
     * @param callback Actions to be executed within the {@link Selenium}
     *            session.
     */
    public void doInSession(ThreadLocal<Selenium> sessionHolder, Runnable callback) {
        startEmbeddedServerIfNecessary();

        Selenium selenium = startSession();
        sessionHolder.set(selenium);

        try {
            callback.run();
        }

        catch (Exception ex) {
            throw new RuntimeException("Test execution failure", ex);
        }

        finally {
            sessionHolder.set(null);
            stopSession(selenium);
        }
    }

    /**
     * Starts a new {@link Selenium} session for the given session
     * configuration.
     * 
     * @return New {@link Selenium} session.
     */
    protected Selenium startSession() {
        Selenium selenium = seleniumFactory.createSelenium(sessionConfig);
        sessionListener.beforeSessionStarted(selenium);

        selenium.start();

        selenium.setTimeout(String.valueOf(sessionConfig.commandTimeoutInSeconds() * 1000));
        selenium.setSpeed(String.valueOf(sessionConfig.executionSpeedInMillis()));
        sessionListener.afterSessionStarted(selenium);

        return selenium;
    }

    /**
     * Stops the given {@link Selenium} session.
     * 
     * @param selenium {@link Selenium} session to be stopped.
     */
    protected void stopSession(Selenium selenium) {
        sessionListener.beforeSessionStopped(selenium);
        selenium.stop();
        sessionListener.afterSessionStopped(selenium);
    }

    /**
     * Starts a new {@link SeleniumServer} instance if the given session
     * configuration tells us to do so.
     */
    protected void startEmbeddedServerIfNecessary() {
        int serverPort = sessionConfig.serverPort();
        boolean serverEnabled = sessionConfig.embeddedServer().enabled();

        synchronized (activeServerPorts) {
            boolean shouldStartServer = serverEnabled && !activeServerPorts.contains(Integer.valueOf(serverPort));

            if (shouldStartServer)
                activeServerPorts.add(Integer.valueOf(serverPort));
            else
                return;
        }

        EmbeddedSeleniumServer serverConfig = sessionConfig.embeddedServer();
        RemoteControlConfiguration rcConfig = new RemoteControlConfiguration();

        rcConfig.setPort(serverPort);
        rcConfig.setSingleWindow(serverConfig.singleWindow());
        rcConfig.setTrustAllSSLCertificates(serverConfig.trustAllSSLCertificates());
        rcConfig.dontTouchLogging();

        try {
            // server shutdown performed via its built-in VM shutdown hook
            SeleniumServer server = createSeleniumServerInstance(serverConfig.slowStaticResources(), rcConfig);
            serverListener.beforeServerStarted(rcConfig);

            server.start();
        }

        catch (BindException bex) {
            // server address/port already in use, nothing to do here
        }

        catch (Exception ex) {
            throw new RuntimeException("Unable to start embedded Selenium server", ex);
        }
    }

    protected SeleniumServer createSeleniumServerInstance(boolean slowResources, RemoteControlConfiguration configuration) throws Exception {
        return new SeleniumServer(slowResources, configuration);
    }

}
